# Copyright (C) 2012 by Sean Sherrard.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Client for Google Data based APIs

A client library for Google's legacy gdata based APIs.
"""

__author__ = 'shermat2@isu.edu (Sean Sherrard)'
__all__ = ['build', 'AtomModel']

import os.path
import re
import json
import xml.etree.cElementTree as etree
from collections import OrderedDict
from httplib2 import Http
from apiclient.discovery import createResource
from apiclient.http import HttpRequest
from apiclient.schema import Schemas
# dynamically import all of the Model classes from apiclient.model
import apiclient.model as _model
for name, obj in vars(_model).iteritems():
	if name.endswith('Model'):
		globals()[name] = obj
del _model

def build(serviceName, http=None, developerKey=None,
		  model=None, requestBuilder=HttpRequest):
	"""Construct a Resource for interacting with a Google Data API.

	Construct a Resource object for interacting with a legacy GData
	API. The serviceName must match a service description file in
	the installation directory.

	Args:
		serviceName: string, name of the service
		http: httplib2.Http, an http client object
		developerKey: string, key obtained
		  from https://code.google.com/apis/console
		model: apiclient.Model, converts to and from the wire format
		requestBuilder: apiclient.http.HttpRequest, encapsulator for
		  an HTTP request

	Returns:
		A Resource object with methods for interacting with
		the service.
	"""

	fname = os.path.join(os.path.dirname(__file__), serviceName + '.json')
	with open(fname) as file:
		service = json.load(file)
	schema = Schemas(service)
	baseUrl = service['baseUrl']
	authDesc = service.get('auth', {})
	if http is None:
		http = Http()
	if model is None:
		if 'model' in service:
			model = globals()[service['model']](schema)
		else:
			model = RawModel()
	extraHeaders = service.get('headers', {})
	globalParams = service.get('parameters')

	# monkeypatch #1: Google apiclient currently ignores global parameters
	#     defined at document level, and instead hard-codes a global
	#     parameter list defined in discovery.py. This copies all of the
	#     global parameters into each method before creating the resource.

	if globalParams:
		def fixMethods(resourceDesc):
			for methodDesc in resourceDesc.get('methods', {}).itervalues():
				methodDesc.setdefault('parameters', {}).update(globalParams)
		fixMethods(service)
		for resourceDesc in service.get('resources', {}).itervalues():
			fixMethods(resourceDesc)
	resource = createResource(http, baseUrl, model, requestBuilder,
							  developerKey, service, None, schema)

	# monkeypatch #2: Google apiclient currently ignores parameter default
	#     values, which are assumed to have a null value. We need to have
	#     explicit default values for the 'projection' and 'visibilty'
	#     parameters, so this sets them before calling the original method.

	# monkeypatch #3: We also need to insert additional headers into the
	#     outgoing request, and modify the processing of the incoming
	#     response, in order to interact correctly with Atom feeds. This
	#     modifies the HttpRequest object returned from the original method.

	def replaceMethod(resourceInst, methodName, defaultParams, respDesc):
		resourceClass = type(resourceInst)
		oldMethod = getattr(resourceClass, methodName)
		def newMethod(self, **kwargs):
			for paramName, paramDesc in defaultParams.iteritems():
				if paramName not in kwargs:
					kwargs[paramName] = paramDesc['default']
			httpRequest = oldMethod(self, **kwargs)
			httpRequest.headers.update(extraHeaders)
			oldPostproc = httpRequest.postproc
			def newPostproc(resp, content):
				oldPostproc.im_self.respDesc = respDesc
				return oldPostproc(resp, content)
			httpRequest.postproc = newPostproc
			return httpRequest
		setattr(newMethod, '__name__', str(methodName))
		setattr(newMethod, '__doc__', oldMethod.__doc__)
		setattr(resourceClass, methodName, newMethod)

	def fixMethods(resourceInst, resourceDesc):
		for methodName, methodDesc in resourceDesc.get('methods', {}).iteritems():
			defaultParams = {paramName: paramDesc for paramName, paramDesc
								 in methodDesc.get('parameters', {}).iteritems()
								 if 'default' in paramDesc}
			respDesc = methodDesc.get('response', {})
			if defaultParams or extraHeaders or respDesc:
				replaceMethod(resourceInst, methodName, defaultParams, respDesc)

	def replaceResource(resourceInst, resourceName, resourceDesc):
		resourceClass = type(resourceInst)
		oldMethod = getattr(resourceClass, resourceName)
		def newMethod(self):
			resource = oldMethod(self)
			fixMethods(resource, resourceDesc)
			return resource
		setattr(newMethod, '__name__', str(resourceName))
		setattr(newMethod, '__is_resource__', True)
		setattr(resourceClass, resourceName, newMethod)

	fixMethods(resource, service)
	for resourceName, resourceDesc in service.get('resources', {}).iteritems():
		replaceResource(resource, resourceName, resourceDesc)
	def auth_method():
		return authDesc
	setattr(resource, 'auth_discovery', auth_method)
	return resource

nsmap = {'atom':  'http://www.w3.org/2005/Atom',
		 'gd':    'http://schemas.google.com/g/2005',
		 'gs':    'http://schemas.google.com/spreadsheets/2006',
		 'gsx':   'http://schemas.google.com/spreadsheets/2006/extended',
		 'batch': 'http://schemas.google.com/gdata/batch',
		 'openSearch': 'http://a9.com/-/spec/opensearch/1.1/'}

def revnsmap(tag):
	uri, sep, local = tag[1:].partition('}')
	if tag[0] != '{' or sep != '}':
		return None, tag
	else:
		return revnsmap.revmap.get(uri), local
revnsmap.revmap = {uri: pref for pref, uri in nsmap.iteritems()}

for pref, uri in nsmap.iteritems():
	etree.register_namespace(pref, uri) 

class AtomModel(BaseModel):
	"""Model class for Atom-based Google Data Protocol.

	Serializes and de-serializes between Atom format and the Python
	object representation of HTTP request and response bodies.
	"""

	accept = 'application/atom+xml'
	content_type = 'application/atom+xml'
	alt_param = 'atom'
	no_content_response = None
	default_ns = 'atom'

	def __init__(self, schema):
		self.schema = schema
		self.regex = re.compile(r'(^[^@].*?)?(?:(?:/@|^@)((?:\w+:)?\w+))?(?:=>(.*))?$')

	def serialize(self, body_value):
		pass

	def deserialize(self, content):
		print 'deserializing', content[:200]
		root = etree.fromstring(content)
		return self._getobj(root, None, self.respDesc), root

	def _getobj(self, elem, name, desc):
		seen = set()
		while '$ref' in desc:
			ref = desc['$ref']
			if ref in seen:
				raise ValueError('loop detected in schema element {}'.format(ref))
			seen.add(ref)
			desc = self.schema.get(ref)
##			val = self.getobj(elem, ref, self.schema.get(ref))
		if 'xpath' in desc:
##			path = desc['xpath']
##			if path[0] == '@':
##				path = './' + path
			path, attr, func = self.regex.match(desc['xpath']).groups()
##			if attr and ':' in attr:
##				pref, local = attr.split(':')
##				attr = '{{{}}}{}'.format(nsmap[pref], local)
##			if func:
##				func = 'val.' + func[1:]
		else:
			path, attr, func = name, None, None
##			attr = func = None
		print 'path before:', path
		if path:
			path = '/'.join(self.default_ns + ':' + p
							if (p not in ('.', '..') and ':' not in p) else p
							for p in path.split('/'))
		print 'path after:', path
#		print 'attr before:', attr
		if attr:
			pref, sep, local = attr.rpartition(':')
			if pref in nsmap:
				attr = '{{{}}}{}'.format(nsmap[pref], local)
#		print 'attr after:', attr
##		if path and path != '.' and ':' not in path:
##			path = self.default_ns + ':' + path
		type = desc.get('type')
		if type in ('string', 'integer', 'number', 'boolean'):
#			print 'elem before:', elem
			if path:
				elem = elem.find(path, nsmap)
#			print 'elem after:', elem
			if elem is None:
				val = None
			elif attr:
##				if ':' in attr:
##					pref, local = attr.split(':', 1)
				val = elem.get(attr)
#				val = elem.find(path, nsmap).get(attr, '')
			else:
				val = elem.text
#				val = elem.findtext(path, '', nsmap)
			if val:
				if func:
					val = eval(func.replace('$', 'val'))
				if type == 'integer':
					val = int(val)
				elif type == 'number':
					val = float(val)
				elif type == 'boolean':
					val = val == 'true'
		elif type == 'object':
			if 'properties' in desc:
				if path:
					elem = elem.find(path, nsmap)
				if elem is None:
					val = None
				else:
					val = {name: self._getobj(elem, name, desc)
							for name, desc in desc['properties'].iteritems()}
			elif 'additionalProperties' in desc:
				first, sep, last = path.rpartition('/')
				pref, sep, local = last.rpartition(':')
				path = (first or '.') + '/' + local
				val = OrderedDict((local, child.text)
									for child in elem.iterfind(path, nsmap)
									  for _pref, local in [revnsmap(child.tag)]
									    if _pref == pref)
			else:
				raise ValueError("schema for object '{}' missing 'properties' element".format(name))
		elif type == 'array':
			if 'items' in desc:
				val = [self._getobj(child, None, desc['items'])
						   for child in elem.iterfind(path, nsmap)]
			else:
				raise ValueError("schema for array '{}' missing 'items' element".format(name))
		else:
			raise ValueError("unknown schema type '{}'".format(type))
		return val

